﻿// Copyright (c) Inossem. All rights reserved.
// Licensed under the LGPL-3.0 license. See LICENSE file in the project root for full license information.
using NamedPipeWrapper.IO;
using NamedPipeWrapper.Threading;
using System;
using System.Collections.Generic;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace NamedPipeWrapper
{
    /// <summary>
    /// Wraps a <see cref="NamedPipeClientStream"/>.
    /// </summary>
    /// <typeparam name="TRead">Reference type to read from the named pipe</typeparam>
    /// <typeparam name="TWrite">Reference type to write to the named pipe</typeparam>
    public class NamedPipeClientExAsync<TRead, TWrite>
        where TRead : class
        where TWrite : class
    {
        /// <summary>
        /// Gets or sets whether the client should attempt to reconnect when the pipe breaks
        /// due to an error or the other end terminating the connection.
        /// Default value is <c>true</c>.
        /// </summary>
        public bool AutoReconnect { get; set; }

        /// <summary>
        /// Invoked whenever a message is received from the server.
        /// </summary>
        public event ConnectionExAsyncMessageEventHandler<TRead, TWrite> ServerMessage;

        /// <summary>
        /// Invoked when the client disconnects from the server (e.g., the pipe is closed or broken).
        /// </summary>
        public event ConnectionExAsyncEventHandler<TRead, TWrite> Disconnected;

        /// <summary>
        /// Invoked whenever an exception is thrown during a read or write operation on the named pipe.
        /// </summary>
        public event PipeExceptionEventHandler Error;

        private readonly string _pipeName;
        private NamedPipeConnectionExAsync<TRead, TWrite> _connection;

        private volatile bool _closedExplicitly;
        /// <summary>
        /// the server name, which client will connect to.
        /// </summary>
        private string _serverName { get; set; }

        /// <summary>
        /// Constructs a new <c>NamedPipeClient</c> to connect to the <see cref="NamedPipeServer{TRead, TWrite}"/> specified by <paramref name="pipeName"/>.
        /// </summary>
        /// <param name="pipeName">Name of the server's pipe</param>
        /// <param name="serverName">the Name of the server, default is  local machine</param>
        public NamedPipeClientExAsync(string pipeName, string serverName = ".")
        {
            _pipeName = pipeName;
            _serverName = serverName;
        }

        public void Connect()
        {
            _closedExplicitly = false;
            connect();
        }

        public void Connect(int timeout)
        {
            _closedExplicitly = false;
            connect(timeout);
        }

        /// <summary>
        /// Connects to the named pipe server asynchronously.
        /// This method returns immediately, possibly before the connection has been established.
        /// </summary>
        public async Task ConnectAsync()
        {
            _closedExplicitly = false;
            await connectAsync();
        }

        public async Task ConnectAsync(int timeout)
        {
            _closedExplicitly = false;
            await connectAsync(timeout);
        }

        public void PushMessage(TWrite message)
        {
            if (_connection != null)
                _connection.PushMessage(message);
        }

        /// <summary>
        ///     Sends a message to the server over a named pipe.
        /// </summary>
        /// <param name="message">Message to send to the server.</param>
        public async Task PushMessageAsync(TWrite message)
        {
            if (_connection != null)
                await _connection.PushMessageAsync(message);
        }

        /// <summary>
        /// Closes the named pipe.
        /// </summary>
        public void Disconnect()
        {
            _closedExplicitly = true;
            if (_connection != null)
                _connection.Close();
        }

        #region Private methods

        private async Task connectAsync()
        {
            var handshake = PipeClientExAsyncFactory.Connect<string, string>(_pipeName, _serverName);
            var dataPipeName = await handshake.ReadObjectAsync();
            handshake.Close();

            // Connect to the actual data pipe
            var dataPipe = PipeClientExAsyncFactory.CreateAndConnectPipe(dataPipeName, _serverName);

            // Create a Connection object for the data pipe
            _connection = ConnectionExAsyncFactory.CreateConnection<TRead, TWrite>(dataPipe);
            _connection.Disconnected += OnDisconnected;
            _connection.ReceiveMessage += OnReceiveMessage;
            _connection.Error += ConnectionOnError;
            _connection.Open();
        }

        private async Task connectAsync(int timeout)
        {
            var handshake = PipeClientExAsyncFactory.Connect<string, string>(_pipeName, _serverName, timeout);
            var dataPipeName = await handshake.ReadObjectAsync();
            handshake.Close();

            // Connect to the actual data pipe
            var dataPipe = PipeClientExAsyncFactory.CreateAndConnectPipe(dataPipeName, _serverName, timeout);

            // Create a Connection object for the data pipe
            _connection = ConnectionExAsyncFactory.CreateConnection<TRead, TWrite>(dataPipe);
            _connection.Disconnected += OnDisconnected;
            _connection.ReceiveMessage += OnReceiveMessage;
            _connection.Error += ConnectionOnError;
            _connection.Open();
        }

        private void connect()
        {
            var handshake = PipeClientExAsyncFactory.Connect<string, string>(_pipeName, _serverName);
            var dataPipeName = handshake.ReadObject();
            handshake.Close();

            // Connect to the actual data pipe
            var dataPipe = PipeClientExAsyncFactory.CreateAndConnectPipe(dataPipeName, _serverName);

            // Create a Connection object for the data pipe
            _connection = ConnectionExAsyncFactory.CreateConnection<TRead, TWrite>(dataPipe);
            _connection.Disconnected += OnDisconnected;
            _connection.ReceiveMessage += OnReceiveMessage;
            _connection.Error += ConnectionOnError;
            _connection.Open();
        }

        private void connect(int timeout)
        {
            var handshake = PipeClientExAsyncFactory.Connect<string, string>(_pipeName, _serverName, timeout);
            var dataPipeName = handshake.ReadObject();
            handshake.Close();

            // Connect to the actual data pipe
            var dataPipe = PipeClientExAsyncFactory.CreateAndConnectPipe(dataPipeName, _serverName, timeout);

            // Create a Connection object for the data pipe
            _connection = ConnectionExAsyncFactory.CreateConnection<TRead, TWrite>(dataPipe);
            _connection.Disconnected += OnDisconnected;
            _connection.ReceiveMessage += OnReceiveMessage;
            _connection.Error += ConnectionOnError;
            _connection.Open();
        }

        protected void OnDisconnected(NamedPipeConnectionExAsync<TRead, TWrite> connection)
        {
            if (Disconnected != null)
                Disconnected(connection);

            // Reconnect
            if (AutoReconnect && !_closedExplicitly)
            {
                var task = ConnectAsync();
                task.Wait();
            }
        }

        protected void OnReceiveMessage(NamedPipeConnectionExAsync<TRead, TWrite> connection, TRead message)
        {
            if (ServerMessage != null)
                ServerMessage(connection, message);
        }

        /// <summary>
        ///     Invoked on the UI thread.
        /// </summary>
        protected void ConnectionOnError(NamedPipeConnectionExAsync<TRead, TWrite> connection, Exception exception)
        {
            OnError(exception);
        }

        /// <summary>
        ///     Invoked on the UI thread.
        /// </summary>
        /// <param name="exception"></param>
        protected void OnError(Exception exception)
        {
            if (Error != null)
                Error(exception);
        }

        #endregion
    }

    static class PipeClientExAsyncFactory
    {
        public static PipeStreamWrapperEx<TRead, TWrite> Connect<TRead, TWrite>(string pipeName, string serverName)
            where TRead : class
            where TWrite : class
        {
            return new PipeStreamWrapperEx<TRead, TWrite>(CreateAndConnectPipe(pipeName, serverName));
        }

        public static PipeStreamWrapperEx<TRead, TWrite> Connect<TRead, TWrite>(string pipeName, string serverName, int timeout)
           where TRead : class
           where TWrite : class
        {
            return new PipeStreamWrapperEx<TRead, TWrite>(CreateAndConnectPipe(pipeName, serverName, timeout));
        }

        public static NamedPipeClientStream CreateAndConnectPipe(string pipeName, string serverName)
        {
            var pipe = createPipe(pipeName, serverName);
            pipe.Connect();
            pipe.ReadMode = PipeTransmissionMode.Message;

            return pipe;
        }

        public static NamedPipeClientStream CreateAndConnectPipe(string pipeName, string serverName, int timeout)
        {
            var pipe = createPipe(pipeName, serverName);
            pipe.Connect(timeout);
            pipe.ReadMode = PipeTransmissionMode.Message;

            return pipe;
        }

        private static NamedPipeClientStream createPipe(string pipeName, string serverName)
        {
            return new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous | PipeOptions.WriteThrough);
        }
    }
}
